/**
 * Axelor Business Solutions
 *
 * Copyright (C) 2017 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.axelor.apps.bankpayment.ebics.client;

import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLException;

import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.axelor.app.AppSettings;
import com.axelor.apps.bankpayment.db.EbicsBank;
import com.axelor.apps.bankpayment.db.repo.EbicsCertificateRepository;
import com.axelor.apps.bankpayment.ebics.interfaces.ContentFactory;
import com.axelor.apps.bankpayment.ebics.service.EbicsCertificateService;
import com.axelor.apps.bankpayment.exception.IExceptionMessage;
import com.axelor.exception.AxelorException;
import com.axelor.exception.db.IException;
import com.axelor.i18n.I18n;


/**
 * A simple HTTP request sender and receiver.
 * The send returns a HTTP code that should be analyzed
 * before proceeding ebics request response parse.
 *
 * @author hachani
 *
 */
public class HttpRequestSender {
 
  private final Logger log = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() );
	
  /**
   * Constructs a new <code>HttpRequestSender</code> with a
   * given ebics session.
   * @param session the ebics session
   */
  public HttpRequestSender(EbicsSession session) {
    this.session = session;
  }

  /**
   * Sends the request contained in the <code>ContentFactory</code>.
   * The <code>ContentFactory</code> will deliver the request as
   * an <code>InputStream</code>.
   *
   * @param request the ebics request
   * @return the HTTP return code
 * @throws AxelorException 
   */
  public final int send(ContentFactory request) throws IOException, AxelorException {
	  
    EbicsBank bank = session.getUser().getEbicsPartner().getEbicsBank();
    String url = bank.getUrl();
    if (url == null || !url.startsWith("http://") && !url.startsWith("https://")) {
    	throw new AxelorException(I18n.get(IExceptionMessage.EBICS_INVALID_BANK_URL),1);
    }
    
    if (bank.getProtocolSelect().equals("ssl")) {
    	return sendSSL(request, bank);
    }
    else {
    	return sendTLS(request, bank);
    }
  }
  
  public final int sendSSL(ContentFactory request, EbicsBank bank) throws AxelorException, IOException {
    String url = bank.getUrl();
  	X509Certificate certificate = EbicsCertificateService.getBankCertificate(bank, EbicsCertificateRepository.TYPE_SSL);
	DefaultHttpClient client = getSecuredHttpClient(certificate, url);
	String proxyConfiguration =  AppSettings.get().get("http.proxy.host");
	
	if (proxyConfiguration != null && !proxyConfiguration.equals("")) {
	  setProxy(client);
	}
	
	InputStream input = request.getContent();
	int retCode = -1;
	log.debug("Bank url: {}", url);
	HttpPost post = new HttpPost(url);
	ContentType type = ContentType.TEXT_XML;
	HttpEntity entity = new InputStreamEntity(input, retCode, type);
	post.setEntity(entity);
	
	try {
		HttpResponse responseHttp = client.execute(post);
		retCode = responseHttp.getStatusLine().getStatusCode();
		log.debug("Http reason phrase: {}" , responseHttp.getStatusLine().getReasonPhrase());
		response = new InputStreamContentFactory(responseHttp.getEntity().getContent());
	} catch (IOException e) {
		e.printStackTrace();
		throw new AxelorException(I18n.get("Connection error: %s"), 1, e.getMessage());
	}
	
	return retCode;
  }
  
  public final int sendTLS(ContentFactory request, EbicsBank bank) throws IOException {
    HttpClient			httpClient;
    PostMethod			method;
    RequestEntity		requestEntity;
    InputStream			input;
    int				retCode;
    httpClient = new HttpClient();
    String proxyConfiguration =  AppSettings.get().get("http.proxy.host");
	
	if (proxyConfiguration != null && !proxyConfiguration.equals("")) {
	  setProxy(httpClient);
	}
    input = request.getContent();
    method = new PostMethod(bank.getUrl());
    method.getParams().setSoTimeout(30000);
    requestEntity = new InputStreamRequestEntity(input);
    method.setRequestEntity(requestEntity);
    method.setRequestHeader("Content-type", "text/xml; charset=ISO-8859-1");
    retCode = -1;
    retCode = httpClient.executeMethod(method);
    response = new InputStreamContentFactory(method.getResponseBodyAsStream());

    return retCode;
  }


  private void setProxy(DefaultHttpClient client) {
	  
	  String			proxyHost;
      int			proxyPort;

      proxyHost =  AppSettings.get().get("http.proxy.host").trim();
      proxyPort = Integer.parseInt(AppSettings.get().get("http.proxy.port").trim());
      if (! AppSettings.get().get("http.proxy.user").equals("")) {
		String				user;
		String				pwd;
		Credentials	credentials;
		AuthScope			authscope;
	
		user =  AppSettings.get().get("http.proxy.user").trim();
		pwd =  AppSettings.get().get("http.proxy.password").trim();
		credentials = new UsernamePasswordCredentials(user, pwd);
		authscope = new AuthScope(proxyHost, proxyPort);
		client.getCredentialsProvider().setCredentials(authscope, credentials);
      }
  }
  
  private void setProxy(HttpClient httpClient) {
	  
      String proxyHost =  AppSettings.get().get("http.proxy.host").trim();
      Integer proxyPort = Integer.parseInt(AppSettings.get().get("http.proxy.port").trim());
      HostConfiguration hostConfig = httpClient.getHostConfiguration();
      hostConfig.setProxy(proxyHost, proxyPort);
      if (! AppSettings.get().get("http.proxy.user").equals("")) {
		String				user;
		String				pwd;
		org.apache.commons.httpclient.UsernamePasswordCredentials	credentials;
		org.apache.commons.httpclient.auth.AuthScope authscope;
	
		user =  AppSettings.get().get("http.proxy.user").trim();
		pwd =  AppSettings.get().get("http.proxy.password").trim();
		credentials = new org.apache.commons.httpclient.UsernamePasswordCredentials(user, pwd);
		authscope = new org.apache.commons.httpclient.auth.AuthScope(proxyHost, proxyPort);
		httpClient.getState().setProxyCredentials(authscope, credentials);
      }

  }


  private DefaultHttpClient getSecuredHttpClient(Certificate cert, String bankURL) throws AxelorException {
    
	HttpParams httpParams = new BasicHttpParams();
	HttpConnectionParams.setConnectionTimeout(httpParams, 30000);
	HttpConnectionParams.setSoTimeout(httpParams, 30000);
	DefaultHttpClient client = new DefaultHttpClient(httpParams);
	
	try {
	    Scheme https  = null;
	    if (cert != null) {
	    	log.debug("SSL certificate exist");
	    	URL url = new URL(bankURL);
	    	log.debug("Url host: {}", url.getHost());
	    	KeyStore keystore  = KeyStore.getInstance("jks");
	 	    char[] password = "NoPassword".toCharArray();
	 	    keystore.load(null, password);
	    	keystore.setCertificateEntry(url.getHost(), cert);
	    	SSLSocketFactory factory = new SSLSocketFactory(keystore);
	    	try {
	    		factory.getHostnameVerifier().verify(url.getHost(), (X509Certificate)cert);
	    		https = new Scheme("https", 443,  new SSLSocketFactory(keystore));
	    	}
	    	catch(SSLException e) {
	    		log.debug("Error in ssl certifcate host name verification");
	    		https = new Scheme("https", 443, SSLSocketFactory.getSocketFactory());
	    	}
	    	
	    }
	    else {
	    	log.debug("SSL certificate not exist");
	    	https = new Scheme("https", 443, SSLSocketFactory.getSocketFactory());
	    }
	    client.getConnectionManager().getSchemeRegistry().register(https);
    } catch(Exception e) {
    	e.printStackTrace();
    	throw new AxelorException(I18n.get("Error adding certificate"), IException.TECHNICAL);
    }
	
	return client;
}

  /**
   * Returns the content factory of the response body
   * @return the content factory of the response.
   */
  public ContentFactory getResponseBody() {
    return response;
  }

  //////////////////////////////////////////////////////////////////
  // DATA MEMBERS
  //////////////////////////////////////////////////////////////////

  private EbicsSession				session;
  private ContentFactory			response;
}
